公開されたPiping Serverを知っている人のみが使えるように限定公開する
やりたいこと
publicなPiping Serverを特定のことを知っている人だけが使えるようにして誰でもが使える状態にはしない方法。
Piping Serverはネット上に公開してどんな場所からも使えるようにしたいけど、みんなが使えるようにするとサーバーの負荷が心配という時に使える。
Hidden Piping Server
追記:rich-piping-serverになった。
以下のような設定ができるPiping Serverを作った。
Basic認証つける。
特定のパスしか転送に使えない。
拒否された場合は、あたかもNginxが500 Internal Server Errorを起こしたようになる。
設定ファイルはホットリロードされる。
以下にそれらを実現するためのリポジトリを作った。内部でnpmパッケージとしてのpiping-serverを使っている。
https://gh-card.dev/repos/nwtgck/rich-piping-server.svg https://github.com/nwtgck/rich-piping-server
使い方は、まず以下のようなconfig.yamlを書く。
code:config.yaml
allowPaths:
- /0s6twklxkrcfs1u
- type: regexp
value: "/abcd+"
basicAuthUsers:
- username: user1
password: pass1234
rejection: nginx-down
(basicAuthUsersは必須フィールドではないのでなくすとBasic認証なしでもできる。詳しいconfigに関しては後述)
あとはnpxコマンドで起動させる。
code:bash
npx nwtgck/hidden-piping-server --config-yaml-path=config.yaml
config.yamlはホットリロードされるようになっていて設定を変えれば自動でロードされて反映される。
上記の設定だと/0s6twklxkrcfs1uや正規表現でマッチする/aacacdbなどで転送は使えるがそれ以外だとNginxが500 Internal Server Error起こしたようなページが出現する。おそらくここにユーザーが訪れても「サーバー落ちてるな笑」ぐらいの気持ちで通り過ぎてくれると思う。
https://gyazo.com/d9d40a41f32cffff9cfbf824c8f3d647
allowPathに/や/helpを追加しないとアクセスしたサーバーがPiping Serverなのかすら分からないと思う。
rejectionをnginx-downの代わりにsocket-closeにするとNginxのエラー画面にならずにソケットを閉じた時の状態になり、以下のような画面になる。
https://gyazo.com/384f45faff2440e41ef9d9bce5eeaedf
Dockerで立てる
Dockerイメージもある。
dockerコマンドがあれば$PWDにconfig.yamlを用意して以下のコマンドをたたくだけでlocalhost:8181に立ち上がる。
code:bash
docker run -p 8181:8080 -v $PWD/config.yaml:/config.yaml nwtgck/rich-piping-server --config-yaml-path=/config.yaml
技術的な話
以下技術的なconfig.yamlの文法をio-tsを使って型付けしていたり、NginxぽいInternal Server Errorを作る話、npmパッケージとしてのpiping-serverを使う話などを書く。
io-tsを使ってTypeScriptの型でJSONをバリデーションする
以下のio-tsを使って設定ファイルのバリデーションをしている。
https://gh-card.dev/repos/gcanti/io-ts.svg https://github.com/gcanti/io-ts
YAMLをロードした後このio-tsでバリデーションしている。
実際には以下のようにしてバリデーション用の値を作っている。configTypeはオブジェクトだがt.TypeOf<typeof configType>することでTypeScriptの型が作れることがポイント。
code:ts
...
export const configType = t.type({
basicAuthUsers: t.union([
t.array(t.type({
username: t.string,
password: t.string,
})),
t.undefined
]),
allowPaths: t.array(
t.union([
t.string,
t.type({
type: t.literal('regexp'),
value: t.string,
})
])
),
rejection: rejectionType,
});
export type Config = t.TypeOf<typeof configType>;
from:
同じようなことをしたくてio-tsを知る前に「JSONをTypeScriptの型でバリデーションしたい! - Qiita」を作っていて、io-tsをもっと早く知りたかった。io-tsの方がエラーメッセージなどをリッチにできそうな気がしたので今回はio-tsを使っている。
TypeScriptの型システムの恩恵を受けられてとても安心して開発できる。実際に以下のようにtype Configは導出される。
https://gyazo.com/debee292d070d53ac870f0786a159f53
Nginxの500 Internal Server Errorを模倣する
以前作っていたポートノッキングのHTTP版のHTTP Knockingで使っていたのでそこから持ってきた。
https://gh-card.dev/repos/nwtgck/http-knocking.svg https://github.com/nwtgck/http-knocking
実際のコードは以下のようになる。ポイントはIEとGoogle Chromeは異なるレスポンスになる現象を発見したのでそれも組み込んであること。上記のconfig.yamlでも好きなNginxのバージョンをでき、その時代のリアルなNginxのエラーが出現させられると思う。本物とNginxと多少の差異はあると思うので、何か見つけたら寄せていきたい。
code:ts
import * as http from "http";
import * as http2 from "http2";
import * as useragent from "useragent";
export function fakeNginxResponse(res: http.ServerResponse | http2.Http2ServerResponse, nginxVersion: string, userAgent: string): void {
// (INFO: Ruby one-liner(localhost:8181 is an actual Nginx Server): puts curl -i -H 'User-Agent: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1)' localhost:8081.split("\r\n").map{|e| (e+"\r\n").inspect}.join(" +\n")
const body =
"<html>\r\n" +
"<head><title>500 Internal Server Error</title></head>\r\n" +
"<body bgcolor=\"white\">\r\n" +
"<center><h1>500 Internal Server Error</h1></center>\r\n" +
<hr><center>nginx/${nginxVersion}</center>\r\n +
"</body>\r\n" +
"</html>\r\n" +
(
useragent.is(userAgent).ie || useragent.is(userAgent).chrome ?
"<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n" +
"<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n" +
"<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n" +
"<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n" +
"<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n" +
"<!-- a padding to disable MSIE and Chrome friendly error page -->\r\n" :
""
);
if ("shouldKeepAlive" in res) {
res.shouldKeepAlive = false;
}
res.writeHead(500, "Internal Server Error", {
"Server": nginx/${nginxVersion},
"Date": new Date().toUTCString(), // (from: https://github.com/nodejs/node/blob/8b4af64f50c5e41ce0155716f294c24ccdecad03/lib/internal/http.js#L9)
"Content-Type": "text/html",
"Content-Length": body.length
});
res.end(body);
}
from:
npmのpiping-serverを使う技術
Piping Serverはnpmで公開されているパッケージでもありライブラリとして利用できる。
例えば、以下のようにリクエストハンドラを作ると/hogepathでないと転送できないようにできる。
code:ts
import {Server as PipingServer} from "piping-server";
function generateHandler({pipingServer, useHttps}: {pipingServer: PipingServer, useHttps: boolean}): Handler {
const pipingHandler = pipingServer.generateHandler(useHttps);
return (req, res) => {
if (req.url !== "/hogepath") {
req.socket.end();
return;
}
pipingHandler(req, res);
};
}
上記のハンドラを使うには以下のようにNode.jsの標準のハンドラに組み込むことができる。
code:ts
import * as log4js from "log4js";
import * as piping from "piping-server";
// Create a piping server
const pipingServer = new piping.Server(logger);
http.createServer(generateHandler({pipingServer, useHttps: false}))
.listen(8080);
リバースプロキシでも実現できるがポートを削減できる点はメリットかもしれない。またNode.jsに慣れている場合はおすすめかもしれない。
rich-piping-serverの--helpを見ると分かるようにpiping-serverに--config-yaml-pathオプションがついたような見た目になる。
code:ヘルプ
Options:
--help Show help boolean
--version Show version number boolean
--http-port Port of HTTP server default: 8080
--enable-https Enable HTTPS default: false
--https-port Port of HTTPS server number
--key-path Private key path string
--crt-path Certification path string
--config-yaml-path Config YAML path string required
これで分かるようにrich-piping-serverに限らず自分好みでPiping Serverをカスタマイズできる。
useHttpsとは
useHttpsはPiping Serverの/helpでhttp://を使うかhttps://を使うか判定するために使っているものでPiping Serverのロジックとは無関係もの。(/helpページは動的にプロトコルやホストなどを埋め込んでなるべくユーザーに優しいヘルプになるようにしている。)
今後
今後隠すことが目的ではなく多機能なPiping Serverを提供することが目的になる可能性もあって、そのときは Rich Piping Server とかに改名すると思う。
追記: Rich Piping Serverに改名された。